A parking lot is a designated area where vehicles can be parked temporarily, either in public or private spaces.
No activity yet
It may consist of multiple floors, and each floor contains a fixed number of parking spots. These spots are often categorized by vehicle size such as small, compact, or large.
When a vehicle enters the parking lot, a parking ticket is issued to record the entry time. Upon exiting, the vehicle owner pays the parking fee.
In this chapter, we will explore the low-level design of a parking lot system in detail.
Let's start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.
Here is an example of how a discussion between the candidate and the interviewer might unfold:
Candidate: Is the parking lot a single-level or multi-level structure?
Interviewer: Let’s assume it is a multi-level parking lot. Each level can have a different number of parking spots.
Candidate: Do we need to support different types of vehicles, such as bikes, cars, and trucks?
Interviewer: Yes, we’ll support at least these three types: bikes, cars, and trucks.
Candidate: Should the system enforce compatibility between vehicle types and parking slot sizes?
Interviewer: Yes, each vehicle must be assigned to a compatible spot type based on its size.
Candidate: Should parking spot assignment be automatic, or should users be able to choose a spot manually?
Interviewer: To keep things simple, let’s use automatic allocation based on availability.
Candidate: Should the system support querying and displaying for open slots?
Interviewer: Yes, users should be able to view open spots based on their vehicle size.
Candidate: Should parking spot IDs follow a specific sequence, like B1, B2, etc., or can they be random?
Interviewer: In real-world systems they are usually ordered, but for this problem, you can assign random IDs.
Candidate: Do we need to track entry and exit times for each vehicle to calculate parking fees, or will it be a flat-rate system?
Interviewer: We should track both entry and exit times. Parking fees will be calculated based on the duration of stay.
Candidate: Do we need to take input from the user, or can we hardcode a sequence of parking requests for this design?
Interview: You can hardcode the sequence for this design. No need for user input handling.
Since the system must enforce compatibility between vehicle types and parking spot sizes, it’s important to clearly define the parking rules.
In real-world scenarios, the following constraints typically apply:
These rules ensure optimal utilization of space and prevent oversized vehicles from occupying undersized spots.
After gathering the details, we can summarize the key system requirements.
After the requirements are clear, the next step is to identify the core entities and objects we will have in our system.
Core entities are the fundamental building blocks of our system. We identify them by analyzing the functional requirements and highlighting key nouns and responsibilities that naturally map to object-oriented abstractions such as classes, enums, or interfaces.
Let’s walk through the functional requirements and extract the relevant entities:
A modern parking lot is typically structured as a multi-level facility, where each floor has its own set of parking spots. These spots may differ in size and are uniquely identified.
To model this layout:
ParkingLot, which aggregates all the floors and provides high-level operations to manage them.ParkingFloor. This entity manages a collection of parking spots, and handles spot-level allocations and queries.ParkingSpot. Each spot has a unique ID, a size category (small, medium, or large), and a status indicating whether it's occupied or available.Together, these three entities form a clear hierarchical model of the system.
ParkingLot
├── ParkingFloor (Floor 1)
│ ├── ParkingSpot (A1 - Small)
│ ├── ParkingSpot (A2 - Medium)
│ └── ...
├── ParkingFloor (Floor 2)
│ ├── ParkingSpot (B1 - Large)
│ └── ...
└── ...Not every vehicle can fit into every parking spot. A truck needs a large spot, while a motorcycle only needs a small one. To ensure proper allocation, the system must recognize vehicle sizes and match them with appropriate spot sizes.
This leads us to the Vehicle entity, which represents any vehicle entering the parking lot. Each vehicle has a license plate and a size, which determines where it can park.
To categorize vehicle sizes consistently, we introduce an enum called VehicleSize, which includes:
SMALL (e.g., motorcycles, scooters)MEDIUM (e.g., sedans, hatchbacks)LARGE (e.g., vans, trucks, SUVs)Why VehicleSize instead of VehicleType (like Bike, Car, Truck)?
Designing the system around vehicle size rather than specific vehicle types makes it easier to maintain, scale, and reason about.
When you use VehicleType, you tie your logic to specific labels like Bike, Car, or Truck. This becomes a problem when a new vehicle type is introduced.
For example, adding a new type like “Mini Truck†would require updating the allocation logic, billing logic, and compatibility checks.
On the other hand, using VehicleSize keeps the focus on the space the vehicle occupies, which is the primary concern in a parking lot. Whether a vehicle is a motorcycle, scooter, or bike is less important than the amount of physical room it needs.
LARGE size.SMALL.MEDIUM.This way, the core logic for allocating spots and calculating fees remains the same. You simply tag each new vehicle with the appropriate size and the system works without any change.
Once a vehicle is assigned a spot, the system needs to record that session. This involves tracking:
To manage this, we introduce the ParkingTicket entity.
A ticket captures the information needed for billing and auditing including vehicle info, spot, entry timestamp, and exit timestamp.
ParkingLot: Aggregates all parking floors and provides a high-level interface for querying and managing the entire facility.ParkingFloor: Represents a single level with multiple parking spots.ParkingSpot: Represents a spot that can accommodate a vehicle. Characterized by size and occupancy status.Vehicle: Captures vehicle-specific details like license number and size.VehicleSize: Defines supported vehicle size categories (SMALL, MEDIUM, LARGE).ParkingTicket: Stores parking session metadata including entry and exit timestamps. Used for billing and audit.These core entities define the key abstractions of the parking lot system and will guide the structure of our low-level design and class diagrams.
Once the core entities have been identified, the next step is to translate them into an organized class structure. This includes defining attributes and responsibilities for each class, identifying relationships among them, and applying relevant design patterns to ensure the system is clean, scalable, and extensible.
We begin by defining the classes and enums, starting with simple data-holding components and progressing toward core classes that encapsulate business logic and system workflows.
Enums are used to define a fixed set of values in a type-safe manner. In our design, we use enums to classify vehicle and spot sizes, which is central to enforcing compatibility rules.
VehicleSizeRepresents the physical size category of a vehicle.
This abstraction allows the system to reason about parking space requirements rather than specific vehicle types.
SMALL, MEDIUM, LARGEData classes are simple containers for storing structured information. They encapsulate basic properties but do not contain complex business logic.
VehicleRepresents a vehicle entering the parking lot.
licenseNumber: StringvehicleSize: VehicleSizeParkingTicketCaptures all metadata related to a parking session. It is generated when a vehicle is parked and updated upon exit.
ticketId: Stringvehicle: Vehiclespot: ParkingSpotentryTime: LocalDateTimeexitTime: LocalDateTime (optional)markExitTime(LocalDateTime exitTime)calculateDuration()These classes form the heart of the system and handle core logic, coordination, and data flow.
ParkingSpotRepresents an individual physical space that can hold a vehicle.
ParkingFloorManages all the spots on a single level of the parking lot.
Attributes:
Methods:
ParkingLotSystemThe top-level class that represents the entire facility. It acts as a Facade for the client.
Attributes:
Methods:
The relationships define how our classes interact to form a cohesive system.
Strong ownership where an object's lifecycle depends on its container.
Weaker ownership where an object can exist independently.
A weaker relationship where one class uses another.
The Strategy Pattern allows an object’s behavior to be selected at runtime by injecting different strategy implementations. This pattern is ideal for pluggable and customizable logic that may vary depending on external factors.
In this design, PricingStrategy is a textbook example of the Strategy Pattern. The interface defines a contract:
double calculateFee(Ticket ticket);
This allows us to create multiple pricing strategies such as:
FlatRatePricing – A constant hourly fee regardless of vehicle size.SizeBasedPricing – Larger vehicles incur higher fees.DurationBasedPricing – Fees are tiered by how long the vehicle was parked.WeekendPricing, DiscountedPricing, etc.The ParkingLot (or ParkingManager) can switch pricing strategies dynamically, for example, based on the day of the week or business rules.
In the current design, spot allocation is simple and rule-based: assign the first available and compatible spot.
However, the same Strategy Pattern can be used in the future to support more complex allocation strategies, such as:
Each of these can implement a common SpotAllocationStrategy interface:
1ParkingSpot allocateSpot(List<ParkingFloor> floors, Vehicle vehicle);This allows the system to adapt to new allocation rules without rewriting core logic.
The Singleton Pattern ensures that a class has only one instance and provides a global access point to that instance. This is particularly useful in systems that represent centralized resources or controllers.
In this design, we may choose to apply the singleton pattern to:
ParkingLot – Since there is typically only one physical parking facility per application instance, it makes sense to restrict this class to a single object in memory.In a command-line simulation or unit test, a singleton ParkingLot ensures that all vehicles interact with the same shared environment.
💡 While singleton is helpful in simple or single-threaded environments, in distributed or concurrent systems, it's advisable to avoid or carefully manage singletons to prevent bottlenecks.
The Facade Pattern provides a simplified, high-level interface to a complex subsystem. It hides underlying components and their interactions from external clients.
In our design, the ParkingLot class serves as an implicit facade by exposing a handful of high-level methods such as:
parkVehicle(Vehicle vehicle)unparkVehicle(Ticket ticket)getAvailableSpotsBySize(VehicleSize size)getAvailableSpotsByFloor(String floorId)These methods encapsulate all the internal logic involving:
External components like ParkingLotSimulator can simply call these methods without needing to understand or interact directly with ParkingFloor, ParkingSpot, Ticket, or PricingStrategy.
VehicleSize Enum1class VehicleSize(Enum):
2 SMALL = "SMALL"
3 MEDIUM = "MEDIUM"
4 LARGE = "LARGE"Defines size categories for vehicles. Parking compatibility is based on this enum:
SMALL: e.g., BikesMEDIUM: e.g., CarsLARGE: e.g., TrucksThis allows the system to enforce which vehicles can fit into which parking spots.
Vehicle HierarchyVehicle is an abstract base class with shared properties for all vehicles. Bike, Car, and Truck extend this class with specific VehicleSize.
1class Vehicle(ABC):
2 def __init__(self, license_number: str, size: VehicleSize):
3 self.license_number = license_number
4 self.size = size
5
6 def get_license_number(self) -> str:
7 return self.license_number
8
9 def get_size(self) -> VehicleSize:
10 return self.size
11
12class Bike(Vehicle):
13 def __init__(self, license_number: str):
14 super().__init__(license_number, VehicleSize.SMALL)
15
16
17class Car(Vehicle):
18 def __init__(self, license_number: str):
19 super().__init__(license_number, VehicleSize.MEDIUM)
20
21
22class Truck(Vehicle):
23 def __init__(self, license_number: str):
24 super().__init__(license_number, VehicleSize.LARGE)This polymorphic design allows the parking logic to work generically with any vehicle.
ParkingSpot Class1class ParkingSpot:
2 def __init__(self, spot_id: str, spot_size: VehicleSize):
3 self.spot_id = spot_id
4 self.spot_size = spot_size
5 self.is_occupied = False
6 self.parked_vehicle = None
7 self._lock = threading.Lock()
8
9 def get_spot_id(self) -> str:
10 return self.spot_id
11
12 def get_spot_size(self) -> VehicleSize:
13 return self.spot_size
14
15 def is_available(self) -> bool:
16 with self._lock:
17 return not self.is_occupied
18
19 def is_occupied_spot(self) -> bool:
20 return self.is_occupied
21
22 def park_vehicle(self, vehicle: Vehicle):
23 with self._lock:
24 self.parked_vehicle = vehicle
25 self.is_occupied = True
26
27 def unpark_vehicle(self):
28 with self._lock:
29 self.parked_vehicle = None
30 self.is_occupied = False
31
32 def can_fit_vehicle(self, vehicle: Vehicle) -> bool:
33 if self.is_occupied:
34 return False
35
36 if vehicle.get_size() == VehicleSize.SMALL:
37 return self.spot_size == VehicleSize.SMALL
38 elif vehicle.get_size() == VehicleSize.MEDIUM:
39 return self.spot_size == VehicleSize.MEDIUM or self.spot_size == VehicleSize.LARGE
40 elif vehicle.get_size() == VehicleSize.LARGE:
41 return self.spot_size == VehicleSize.LARGE
42 else:
43 return FalseRepresents a physical parking spot on a floor.
canFitVehicle() enforces the sizing rules, e.g., a small vehicle can't occupy a large-only spot.ParkingFloor1class ParkingFloor:
2 def __init__(self, floor_number: int):
3 self.floor_number = floor_number
4 self.spots: Dict[str, ParkingSpot] = {}
5 self._lock = threading.Lock()
6
7 def add_spot(self, spot: ParkingSpot):
8 self.spots[spot.get_spot_id()] = spot
9
10 def find_available_spot(self, vehicle: Vehicle) -> Optional[ParkingSpot]:
11 with self._lock:
12 available_spots = [
13 spot for spot in self.spots.values()
14 if not spot.is_occupied_spot() and spot.can_fit_vehicle(vehicle)
15 ]
16 if available_spots:
17 # Sort by spot size (smallest first)
18 available_spots.sort(key=lambda x: x.get_spot_size().value)
19 return available_spots[0]
20 return None
21
22 def display_availability(self):
23 print(f"--- Floor {self.floor_number} Availability ---")
24 available_counts = defaultdict(int)
25
26 for spot in self.spots.values():
27 if not spot.is_occupied_spot():
28 available_counts[spot.get_spot_size()] += 1
29
30 for size in VehicleSize:
31 print(f" {size.value} spots: {available_counts[size]}")Represents one floor of the parking lot. Manages a list of parking spots.
Provides:
findAvailableSpot() for locating suitable spots for a vehicle.displayAvailability() for real-time spot count per size type.ParkingTicket1class ParkingTicket:
2 def __init__(self, vehicle: Vehicle, spot: ParkingSpot):
3 self.ticket_id = str(uuid.uuid4())
4 self.vehicle = vehicle
5 self.spot = spot
6 self.entry_timestamp = int(time.time() * 1000)
7 self.exit_timestamp = 0
8
9 def get_ticket_id(self) -> str:
10 return self.ticket_id
11
12 def get_vehicle(self) -> Vehicle:
13 return self.vehicle
14
15 def get_spot(self) -> ParkingSpot:
16 return self.spot
17
18 def get_entry_timestamp(self) -> int:
19 return self.entry_timestamp
20
21 def get_exit_timestamp(self) -> int:
22 return self.exit_timestamp
23
24 def set_exit_timestamp(self):
25 self.exit_timestamp = int(time.time() * 1000)
26Captures details of a parking transaction.
Strategy pattern for fee calculation. Allows swapping between different pricing models.
1class FeeStrategy(ABC):
2 @abstractmethod
3 def calculate_fee(self, parking_ticket: ParkingTicket) -> float:
4 pass
5
6class FlatRateFeeStrategy(FeeStrategy):
7 RATE_PER_HOUR = 10.0
8
9 def calculate_fee(self, parking_ticket: ParkingTicket) -> float:
10 duration = parking_ticket.get_exit_timestamp() - parking_ticket.get_entry_timestamp()
11 hours = (duration // (1000 * 60 * 60)) + 1
12 return hours * self.RATE_PER_HOUR
13
14class VehicleBasedFeeStrategy(FeeStrategy):
15 HOURLY_RATES = {
16 VehicleSize.SMALL: 10.0,
17 VehicleSize.MEDIUM: 20.0,
18 VehicleSize.LARGE: 30.0
19 }
20
21 def calculate_fee(self, parking_ticket: ParkingTicket) -> float:
22 duration = parking_ticket.get_exit_timestamp() - parking_ticket.get_entry_timestamp()
23 hours = (duration // (1000 * 60 * 60)) + 1
24 return hours * self.HOURLY_RATES[parking_ticket.get_vehicle().get_size()]FlatRateFeeStrategy charges a constant hourly rate for all vehicles. VehicleBasedFeeStrategy Fee is dynamic based on vehicle size. Larger vehicles pay more per hour.
ParkingStrategyUses strategy pattern to encapsulate different algorithms to find a parking spot from available floors.
1class ParkingStrategy(ABC):
2 @abstractmethod
3 def find_spot(self, floors: List[ParkingFloor], vehicle: Vehicle) -> Optional[ParkingSpot]:
4 pass
5
6class NearestFirstStrategy(ParkingStrategy):
7 def find_spot(self, floors: List[ParkingFloor], vehicle: Vehicle) -> Optional[ParkingSpot]:
8 for floor in floors:
9 spot = floor.find_available_spot(vehicle)
10 if spot is not None:
11 return spot
12 return None
13
14class FarthestFirstStrategy(ParkingStrategy):
15 def find_spot(self, floors: List[ParkingFloor], vehicle: Vehicle) -> Optional[ParkingSpot]:
16 reversed_floors = list(reversed(floors))
17 for floor in reversed_floors:
18 spot = floor.find_available_spot(vehicle)
19 if spot is not None:
20 return spot
21 return None
22
23class BestFitStrategy(ParkingStrategy):
24 def find_spot(self, floors: List[ParkingFloor], vehicle: Vehicle) -> Optional[ParkingSpot]:
25 best_spot = None
26
27 for floor in floors:
28 spot_on_this_floor = floor.find_available_spot(vehicle)
29
30 if spot_on_this_floor is not None:
31 if best_spot is None:
32 best_spot = spot_on_this_floor
33 else:
34 # A smaller spot size enum ordinal means a tighter fit
35 if list(VehicleSize).index(spot_on_this_floor.get_spot_size()) < list(VehicleSize).index(best_spot.get_spot_size()):
36 best_spot = spot_on_this_floor
37
38 return best_spotParkingLot (Central Controller)This class acts as the main entry point and orchestrator for the entire system. It uses the Singleton pattern to ensure only one instance of the parking lot exists and the Facade pattern to provide a simple interface to a complex subsystem.
1class ParkingLot:
2 _instance = None
3 _lock = threading.Lock()
4
5 def __init__(self):
6 if ParkingLot._instance is not None:
7 raise Exception("This class is a singleton!")
8 self.floors: List[ParkingFloor] = []
9 self.active_tickets: Dict[str, ParkingTicket] = {}
10 self.fee_strategy = FlatRateFeeStrategy()
11 self.parking_strategy = NearestFirstStrategy()
12 self._main_lock = threading.Lock()
13
14 @staticmethod
15 def get_instance():
16 if ParkingLot._instance is None:
17 with ParkingLot._lock:
18 if ParkingLot._instance is None:
19 ParkingLot._instance = ParkingLot()
20 return ParkingLot._instance
21
22 def add_floor(self, floor: ParkingFloor):
23 self.floors.append(floor)
24
25 def set_fee_strategy(self, fee_strategy: FeeStrategy):
26 self.fee_strategy = fee_strategy
27
28 def set_parking_strategy(self, parking_strategy: ParkingStrategy):
29 self.parking_strategy = parking_strategy
30
31 def park_vehicle(self, vehicle: Vehicle) -> Optional[ParkingTicket]:
32 with self._main_lock:
33 spot = self.parking_strategy.find_spot(self.floors, vehicle)
34 if spot is not None:
35 spot.park_vehicle(vehicle)
36 ticket = ParkingTicket(vehicle, spot)
37 self.active_tickets[vehicle.get_license_number()] = ticket
38 print(f"Vehicle {vehicle.get_license_number()} parked at spot {spot.get_spot_id()}")
39 return ticket
40 else:
41 print(f"No available spot for vehicle {vehicle.get_license_number()}")
42 return None
43
44 def unpark_vehicle(self, license_number: str) -> Optional[float]:
45 with self._main_lock:
46 ticket = self.active_tickets.pop(license_number, None)
47 if ticket is None:
48 print(f"Ticket not found for vehicle {license_number}")
49 return None
50
51 ticket.get_spot().unpark_vehicle()
52 ticket.set_exit_timestamp()
53 fee = self.fee_strategy.calculate_fee(ticket)
54 print(f"Vehicle {license_number} unparked from spot {ticket.get_spot().get_spot_id()}")
55 return feeThe ParkingLotDemo class demonstrates how a client would interact with the system, showcasing its full functionality.
1class ParkingLotDemo:
2 @staticmethod
3 def main():
4 parking_lot = ParkingLot.get_instance()
5
6 # 1. Initialize the parking lot with floors and spots
7 floor1 = ParkingFloor(1)
8 floor1.add_spot(ParkingSpot("F1-S1", VehicleSize.SMALL))
9 floor1.add_spot(ParkingSpot("F1-M1", VehicleSize.MEDIUM))
10 floor1.add_spot(ParkingSpot("F1-L1", VehicleSize.LARGE))
11
12 floor2 = ParkingFloor(2)
13 floor2.add_spot(ParkingSpot("F2-M1", VehicleSize.MEDIUM))
14 floor2.add_spot(ParkingSpot("F2-M2", VehicleSize.MEDIUM))
15
16 parking_lot.add_floor(floor1)
17 parking_lot.add_floor(floor2)
18
19 parking_lot.set_fee_strategy(VehicleBasedFeeStrategy())
20
21 # 2. Simulate vehicle entries
22 print("\n--- Vehicle Entries ---")
23 floor1.display_availability()
24 floor2.display_availability()
25
26 bike = Bike("B-123")
27 car = Car("C-456")
28 truck = Truck("T-789")
29
30 bike_ticket = parking_lot.park_vehicle(bike)
31 car_ticket = parking_lot.park_vehicle(car)
32 truck_ticket = parking_lot.park_vehicle(truck)
33
34 print("\n--- Availability after parking ---")
35 floor1.display_availability()
36 floor2.display_availability()
37
38 # 3. Simulate another car entry (should go to floor 2)
39 car2 = Car("C-999")
40 car2_ticket = parking_lot.park_vehicle(car2)
41
42 # 4. Simulate a vehicle entry that fails (no available spots)
43 bike2 = Bike("B-000")
44 failed_bike_ticket = parking_lot.park_vehicle(bike2)
45
46 # 5. Simulate vehicle exits and fee calculation
47 print("\n--- Vehicle Exits ---")
48
49 if car_ticket is not None:
50 fee = parking_lot.unpark_vehicle(car.get_license_number())
51 if fee is not None:
52 print(f"Car C-456 unparked. Fee: ${fee:.2f}")
53
54 print("\n--- Availability after one car leaves ---")
55 floor1.display_availability()
56 floor2.display_availability()
57
58
59if __name__ == "__main__":
60 ParkingLotDemo.main()This driver code simulates a real-world scenario by:
Which design pattern is most suitable for providing different parking fee calculation strategies in a parking lot system?
No comments yet. Be the first to comment!